import logging,coloredlogs,sys,os,time,requests,subprocess,threading,uuid,signal
# from loguru import logger
from multiprocessing import current_process
import json,yaml
from tqdm import tqdm
gpu_num=os.environ.get('CUDA_VISIBLE_DEVICES',0)
font_path="msyh.ttc"
def get_app_dir():
    base_ext = os.path.splitext(os.path.basename(sys.argv[0]))[1]
    # print("argv:",sys.argv)
    app_dir=""
    if base_ext == '.exe':
        # Nuitka 打包或 PyInstaller 打包
        # if hasattr(sys, '_MEIPASS'):
        #     # Nuitka 单文件模式
        #     return sys._MEIPASS
        app_dir= os.path.dirname(sys.argv[0])
    else:
        app_dir= os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    # os.chdir(app_dir)
    # print("应用目录:",app_dir)
        if app_dir not in sys.path:
            sys.path.append(app_dir)
    # print("sys.path:",sys.path)
    return app_dir
run_dir=os.path.dirname(__file__)
app_dir=get_app_dir()
app_tmp_dir=os.path.join(app_dir,"tmp")
app_config_dir=os.path.join(app_dir,'config')
app_exit_event=threading.Event()
app_update_event=threading.Event()
tmpdir=os.path.join(app_tmp_dir,f"process{gpu_num}")
os.makedirs(tmpdir,exist_ok=True)
os.makedirs(app_config_dir,exist_ok=True)
current_process_name=current_process().name
ppid=os.getppid()
pid=os.getpid()
# logid=uuid.uuid4().hex[:8]
# print("logid:",logid,__file__)
# print("当前进程ID:",pid,"父进程ID:",ppid)
if ppid==pid:
    # 主进程
    current_process_name="app"
    pass
log_cast_time=True
class DeltaTimeFilter(logging.Filter):
    def __init__(self):
        super().__init__()
        self.last_time = time.time()

    def filter(self, record):
        current = time.time()
        cost= current - self.last_time
        if cost>1:
            record.delta=f"{cost:>7.2f}s"  # 超过1秒的时间差
            pass
        else:
            record.delta = f"{cost*1000:>6.2f}ms" 
        # record.delta = (current - self.last_time) * 1000
        self.last_time = current
        return True
    
delta_filter=DeltaTimeFilter()
log_format_str='%(asctime)s [%(process)d:%(processName)s] %(levelname)s %(message)s'
# %(delta)6.2fms
if log_cast_time:
    log_format_str='%(asctime)s [%(delta)s] [%(process)d:%(processName)s] %(levelname)s %(message)s'
log_format=logging.Formatter(log_format_str)

logging_reset_names=["paddleocr","paddlex","fastapi","asyncio","uvicorn", "uvicorn.error", "uvicorn.access", "fastapi"]
def logging_config(date_cast=False):
    from logging.handlers import RotatingFileHandler
    global logging_reset_names
    info_log_file=os.path.join(tmpdir,f'{current_process_name}.log')
    # 按文件大小轮转（最大1MB，保留3个备份）
    info_handler=RotatingFileHandler(info_log_file,maxBytes=10*1024*1024, backupCount=3,encoding='utf-8')
    #设置 RotatingFileHandler格式化
    # 设置日志格式
    info_handler.setFormatter(log_format)
    info_handler.setLevel(logging.DEBUG)
    info_handler.addFilter(delta_filter)
    # error_log_file=os.path.join(tmpdir,f'{current_process_name}_error.log')
    # error_handler=RotatingFileHandler(error_log_file, maxBytes=2*1024*1024, backupCount=1,encoding='utf-8')
    # error_handler.setLevel(logging.ERROR)
    # error_handler.setFormatter(logging.Formatter(log_format_str))
    # error_handler.addFilter(DeltaTimeFilter())
    steam_handler=logging.StreamHandler()
    steam_handler.addFilter(delta_filter)
    steam_handler.setLevel(logging.DEBUG)
    handlers = [
        info_handler,
        # error_handler,
        steam_handler
    ]
    logging.basicConfig(level=logging.INFO,handlers=handlers)
    def print_style_log(logger_method):
        """将logger方法转换为print风格"""
        def wrapper(*args, **kwargs):
            try:
                sep = kwargs.pop('sep', ' ')
                message = sep.join(str(arg) for arg in args)
                logger_res= logger_method(message, **kwargs)
            except Exception as e:
                print("logging print error:",e, args, kwargs)
                pass
        return wrapper
    info_print = print_style_log(logging.info)
    warn_print = print_style_log(logging.warning)
    error_print = print_style_log(logging.error)
    debug_print = print_style_log(logging.debug)
    # 必须在驱动设置完成后调整颜色方案.不然无法写入文件
    coloredlogs.install(
        level='INFO',  # 设置默认日志级别
        # fmt='%(asctime)s  [%(name)s] %(levelname)s %(message)s',  # 日志格式
        # [%(delta)6.2fms]
        fmt=log_format_str,  # 日志格式
        datefmt='%H:%M:%S',  # 时间格式
        field_styles={
            'asctime': {'color': 'green'},
            'name': {'color': 'blue'},
            'levelname': {'color': 'black', 'bold': True},
        },
        level_styles={
            'debug': {'color': 'cyan'},
            'info': {'color': 'green'},
            'warning': {'color': 'yellow'},
            'error': {'color': 'red'},
            'critical': {'color': 'red', 'bold': True},
        }
    )
    # info_print("日志文件:", info_log_file)
    return info_print,warn_print,error_print,debug_print
    pass
info_print,warn_print,error_print,debug_print=logging_config(log_cast_time)
def logging_reset_config():
    info_print("日志驱动重置")
    for logger_name in logging_reset_names:
        logger = logging.getLogger(logger_name)
        for handler in logger.handlers:
            info_print(f"重置{logger_name} logger handler:",handler)
            handler.setFormatter(log_format)
            handler.addFilter(delta_filter)
        # logger.handlers.clear()  # 清空原有 handler
        # logger.propagate = False  # 禁止日志冒泡
        coloredlogs.install(
            level='INFO',  # 设置默认日志级别
            logger=logger,
            # fmt='%(asctime)s  [%(name)s] %(levelname)s %(message)s',  # 日志格式
            # [%(delta)6.2fms]
            fmt=log_format_str,  # 日志格式
            datefmt='%H:%M:%S',  # 时间格式
            field_styles={
                'asctime': {'color': 'green'},
                'name': {'color': 'blue'},
                'levelname': {'color': 'black', 'bold': True},
            },
            level_styles={
                'debug': {'color': 'cyan'},
                'info': {'color': 'green'},
                'warning': {'color': 'yellow'},
                'error': {'color': 'red'},
                'critical': {'color': 'red', 'bold': True},
            }
        )
    pass

# info_print("缓存目录:",tmpdir)
# info_print("应用目录:",app_dir)
# info_print(f"应用 PID: {ppid} ID: {pid}")
# info_print("校验时间:",time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))

_session=requests.Session()
_thread=None
class ThreadTool():
    _instance = None
    def __new__(cls):
        """单例模式

        Returns:
            _type_: _description_
        """
        global _thread
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance = ThreadTool()
            info_print("创建清理线程实例")
        return cls._instance
    def __init__(self):
        self._events=[]
        # 清理间隔
        self.event=self.event_init()
        self._time_clear_interval=5*60  # 默认1分钟
        self._destroy=False
        self._clear_dir={}
        pass
    def __del__(self):
        info_print("清理线程销毁",self.url)
        self.destroy()
    
    def event_init(self):
        e=threading.Event()
        self._events.append(e)
        return e
    def event_clear(self):
        for e in self._events:
            e.clear()
    def event_set(self):
        for e in self._events:
            e.set()
    def destroy(self):
        if self._destroy:
            return
        self.event_set()
        self._destroy=True       
    def register_dir_clear(self,dir_path,save_day):
        self._clear_dir[dir_path]=save_day
        warn_print("注册清理目录:",dir_path,"保存天数:",save_day)
        pass
    def thread_dir_clear(self):
        # 删除过期文件
        # 清理评率
        import tool.process_tool as process_tool
        time.sleep(.3)
        info_print("开启文件清理线程")
        time_clear=time.time()
        first_run=True
        time_clear_interval=self._time_clear_interval
        while True:
            if self.event.is_set():
                info_print("thread_dir_clear 停止")
                break
            now_time=time.time()
            if first_run or (now_time - time_clear > time_clear_interval):
                if len(self._clear_dir) > 0:
                    first_run=False
                    time_clear=now_time
                    clear_dir=self._clear_dir.copy()
                    for dir_path,save_day in clear_dir.items():
                        if not os.path.exists(dir_path) or not os.path.isdir(dir_path):
                            # warn_print("清理目录不存在:",dir_path)
                            continue
                        # info_print(f"文件清理开始,频率:{time_clear_interval}s,保存:{save_day}天,路径:{dir_path}")
                        # del_info=dir_clear(dir_path,save_day=save_day)
                        process_tool.get_pool().starmap_async_once(dir_clear, dir_path, save_day)
                        # info_print(f"文件清理开始,频率:{time_clear_interval}s,保存:{save_day}天,路径:{dir_path}")
                        # info_print(f"文件清理:...{dir_path[-20:]},保留:{save_day}d,文件总计:{file_count},磁盘占用:{del_info['file_size_format']} 删除文件:{file_expire_count}")
            time.sleep(.3)
        pass
        warn_print("文件清理线程结束")
    
    def thread_start(self):
        threading.Thread(name="thread_dir_clear",target=self.thread_dir_clear, daemon=True).start()
        pass

class ThreadEventTaskClass():
    """
    直播检测任务
    """
    def __init__(self):
        self._events=[]
        # 总进程状态
        self.event= self.event_init()
        self._destroy=False
        pass
    def __del__(self):
        # info_print("任务销毁",self.url)
        self.destroy()
    def event_init(self):
        e=threading.Event()
        self._events.append(e)
        return e
    def event_clear(self):
        for e in self._events:
            e.clear()
    def event_set(self):
        for e in self._events:
            e.set()
    
    def destroy(self):
        if self._destroy:
            return
        self._destroy=True
        info_print("开始任务销毁")
        self.event.set()
        self.event_set()
        pass


def threads_info(detail=False):
    """获取当前线程信息"""
    threads=threading.enumerate()
    warn_print(f"当前线程数量: {len(threads)}")
    if detail:
        for t in threads:
            info_print(f"线程名: {t.name}, 标识: {t.ident}, 是否主线程: {t is threading.main_thread()},活跃状态: {t.is_alive()}, 守护线程: {t.daemon}")
    # 如果你在线程创建时设置了 name 或 target，可以这样查看
    # if hasattr(t, '_target'):
    #     info_print(f"  目标函数: {t._target}, 参数: {getattr(t, '_args', None)}")
    return threads
def get_thread_tool()->ThreadTool:
    import tool.process_tool as process_tool
    global _thread
    process_tool.get_pool()
    if _thread is None:
        _thread = ThreadTool()
        _thread.thread_start()
    return _thread
def session_request(**kwargs):
    # info_print("request:",kwargs)
    try:
        return _session.request(proxies={
                            "http": None,
                            "https": None
                        },
                        verify=False,
                        **kwargs)
    except Exception as e:
        info_print("request error:",e,kwargs)

def get_external_ip(*args):
    try:
        # https://github.com/ihmily/ip-info-api
        # http://ifconfig.me
        # https://2025.ip138.com/
        # logging.info(args)
        response = requests.get('http://httpbin.org/ip')
        if response.status_code == 200:
            return response.json().get('origin')
        else:
            return "Failed to retrieve IP address"
    except Exception as e:
        return str(e)

def time_uuid():
    """随机值

    Returns:
        _type_: _description_
    """
    mictime=str(time.time())
    time_uuid= time.strftime("%Y%m%d%H%M%S", time.localtime())
    return f"{time_uuid}_{mictime[11:15]}"
def file_check_dirs(filename):
    filedir = os.makedirs(os.path.dirname(filename),exist_ok=True)
    return filename
def save_yaml(yamlfile, default: dict = {}, topath=tmpdir):
    tofile = file_check_dirs(os.path.join(topath, yamlfile))
    with open(tofile, "w", encoding='utf-8') as openfile:
        yaml.dump(default, openfile, allow_unicode=True)
    return tofile
def get_yaml(yamlfile, default: dict = None, topath=tmpdir):
    # if os.path.exists(yamlfile):
    #     tofile=os.path.realpath(yamlfile)
    #     pass
    # else:
    tofile = file_check_dirs(os.path.join(topath, yamlfile))
    if os.path.exists(tofile):
        with open(tofile, 'r', encoding='utf-8') as openfile:
            try:
                res = yaml.load(openfile.read(), Loader=yaml.FullLoader)
            except:
                info_print("载入yaml异常:", tofile)
                res = None
                pass
            if res is None:
                return default
            return res
    return default
def save_json(jsonfile, jsondata, topath=tmpdir):
    tofile = file_check_dirs(os.path.join(topath, jsonfile))
    with open(tofile, 'w', encoding='utf-8') as openfile:
        json.dump(jsondata, openfile, ensure_ascii=False)
    return tofile
def get_json(jsonfile, default: dict = None, topath=tmpdir):
    tofile = file_check_dirs(os.path.join(topath, jsonfile))
    if os.path.exists(tofile):
        with open(tofile, 'r', encoding='utf-8') as openfile:
            return json.load(openfile)
    return default
def list_file_path(dir_path, regex="G_*.pth",file_name_only=False):
    import glob
    f_list = glob.glob(os.path.realpath( os.path.join(dir_path, regex)))
    f_list.sort(key=lambda f: int("".join(filter(str.isdigit, f))))
    if file_name_only:
        f_list = [os.path.basename(f) for f in f_list]
    # x = f_list[-1]
    return f_list
def download_file(url, filename,filesize=0,file_modify_check=None):
    r = requests.get(url, stream=True)
    # print("header:", url, r.headers)
    # info_print("获取文件长度:")
    r.raise_for_status()
    filelength = int(r.headers['Content-Length'])
    file_modify_time = r.headers.get('Last-Modified', None)
    file_modify=""
    file_mtime=0
    todownload=True
    # info_print("获取文件长度 ok:",filelength,file_modify_time)
    if file_modify_time is not None:
        file_modify_time = time.strptime(file_modify_time, '%a, %d %b %Y %H:%M:%S GMT')
        file_modify_time = time.mktime(file_modify_time)
        file_modify=time.strftime("%Y-%m-%d %H:%M:%S",time.localtime(file_modify_time))
        # info_print("文件修改时间:",file_modify_time,time.strftime("%Y-%m-%d %H:%M:%S",time.localtime(file_modify_time)))
    
    try:
        filename=os.path.abspath(filename)
        os.makedirs(os.path.dirname(filename),exist_ok=True)
        if filesize==0 and os.path.exists(filename):
            filesize=os.path.getsize(filename)
            # print("download:",filelength,filesize)
        if filelength==filesize:
            # info_print("文件相同.跳过下载:",filesize,filelength)
            todownload=False
        if file_modify_check is not None and file_modify and file_modify_check!=file_modify:
            # info_print("文件有修改.开始下载:",url,file_modify_check,file_modify)
            todownload=True
        if todownload:
            with open(filename, 'wb') as f:
                pbar = tqdm(total=int(filelength),
                            desc="下载:" + os.path.basename(filename)[:10],
                            unit='B',
                            unit_scale=True,
                            unit_divisor=1024)
                for chunk in r.iter_content(chunk_size=1024):
                    if chunk:  # filter out keep-alive new chunks
                        pbar.update(len(chunk))
                        # if cb is not None:
                        #     cb(pbar)
                        f.write(chunk)
        pass
    except Exception as e:
        info_print("下载失败:",url,filename,str(e))
        if os.path.exists(filename):
            os.remove(filename)
        pass
    return filename,filelength,todownload,file_modify
def download_font():
    # http://oss.580u.com/ai-public/yolov8/msyh.ttc
    if os.path.exists(font_path):
        return font_path
    download_file("http://oss.580u.com/ai-public/yolov8/msyh.ttc", font_path)
    info_print("下载字体文件完成:", font_path)
    return font_path
def format_file_size(size_bytes):
    """
    将字节大小格式化为可读的字符串 (B, KB, MB, GB, TB)
    """
    if size_bytes == 0:
        return "0 B"
    size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
    i = 0
    while size_bytes >= 1024 and i < len(size_name) - 1:
        size_bytes /= 1024.0
        i += 1
    return f"{size_bytes:.2f} {size_name[i]}"
def is_file_locked(filepath):
    try:
        # 以独占方式打开文件（只要能打开就说明没被锁）
        with open(filepath, 'a'):
            pass
        return False  # 没有被锁定
    except PermissionError as e:
        if e.winerror == 32:
            return True  # 被锁定
        raise
    except Exception:
        return True  # 其它异常也视为被锁定
def dir_clear(dir_name="",save_day=1):
    
    res=dict(file_count=0, file_expire_count=0, file_size=0, file_size_format='')
    if dir_name=="":
        warn_print("录像目录未设置,无法清理")
        return res
    if not os.path.exists(dir_name):
        # warn_print("录像目录不存在:",dir)
        return res
    
    now_time=time.time()
    expire_time=now_time-(save_day*24*60*60)
    file_count=0
    file_size=0
    file_expire_count=0
    
    for root, dirs, files in os.walk(dir_name):
        for file in files:
            try:
                file_path = os.path.join(root, file)
                if os.path.getmtime(file_path) < expire_time:
                    warn_print("删除过期文件:",file_path)
                    os.remove(file_path)
                    file_expire_count+=1
                else:
                    file_count+=1
                    file_size+=os.path.getsize(file_path)
            except Exception as e:
                error_print("删除过期文件异常:",e)
    # 清理空目录
    
    try:
        for root, dirs, files in os.walk(dir_name, topdown=False):
            for sub_dir in dirs:
                dir_path = os.path.join(root, sub_dir)
                if not os.listdir(dir_path):
                    warn_print("删除空目录:",dir_path)
                    os.rmdir(dir_path)
    except Exception as e:
        error_print("删除空目录异常:",e)
    pass
    file_size_format=format_file_size(file_size)
    warn_print(f"文件清理:...{dir_name[-30:]},保留:{save_day}d,文件总计:{file_count},磁盘占用:{file_size_format} 删除文件:{file_expire_count}")
    return dict(file_count=file_count, file_expire_count=file_expire_count, file_size=file_size, file_size_format=file_size_format)
    # return file_count, file_expire_count,file_size
# md5 加密工具
def md5_str(s):
    import hashlib
    if isinstance(s, str):
        s = s.encode('utf-8')
    m = hashlib.md5()
    m.update(s)
    return m.hexdigest()

if __name__=="__main__":
    # 测试日志输出
    logging.debug("这是一条调试信息")
    logging.info("这是一条普通信息")
    logging.warning("这是一条警告信息")
    logging.error("这是一条错误信息")
    logging.critical("这是一条严重错误信息")
    info_print("测试print")
    while True:
        print(range(10))
        time.sleep(1)
    # client_version_update("https://oss.580u.com/ai-public/yolov8/launch_module.cp310-win_amd64.pyd")